/* NOTICE
    OpenJaWS - Open Java Weather Station
    
    Copyright (C) 2008 Grant Gardner <grant@lastweekend.com.au>
    
    OpenJaWS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
NOTICE */

package au.com.lastweekend.openjaws.api;

import java.io.Serializable;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.com.lastweekend.openjaws.units.RainfallUnit;
import au.com.lastweekend.openjaws.units.Unit;
import au.com.lastweekend.openjaws.util.StringUtil;

/**
 * Simple structure for capturing readings from a WeatherStation
 * 
 * Holds current/max/min for a fixed set of readings as well as a Map<String,Object> of other station values.
 * 
 * Manages an incremental rainfall count if over multiple readings.
 * 
 * All of the get methods that return other structures are protected from returning null
 * 
 * 
 * @author ggardner
 * 
 */
public class Readings implements Serializable {

    private static final String HEADING_FORMAT = "%-19s,%-11s,%-11s,%-11s,%-11s,%-15s,%-12s,%-11s,%-11s,%-11s,%-11s\n";
    private static final String VALUES_FORMAT = "%10tF %<8tT,%11.2s,%11.2s,%11.0s,%11.0s,%15.2s,%12.1s,%11.2s,%11.2s,%11.2s,%11.2s\n";
    private static final String MIN_MAX_VALUES_FORMAT = "%-19s,%11.2s,%11.2s,%11.0s,%11.0s,%15.2s,%12.1s,%11.2s,%11.2s\n";
    private static final String MIN_MAX_DATES_FORMAT = "%19s,%11tF,%11tF,%11tF,%11tF,%15tF,%12tF,%11tF,%11tF\n";
    private static final String MIN_MAX_TIMES_FORMAT = "%19s,%11tT,%11tT,%11tT,%11tT,%15tT,%12tT,%11tT,%11tT\n";

    private static final Logger LOG = LoggerFactory.getLogger(Readings.class);

    private Date readingTime = null;
  
    private CurrentMinMax indoorTemperature = new CurrentMinMax();
    private CurrentMinMax indoorHumidity = new CurrentMinMax();
    private CurrentMinMax outdoorTemperature = new CurrentMinMax();
    private CurrentMinMax outdoorHumidity = new CurrentMinMax();
    private CurrentMinMax airPressure = new CurrentMinMax();
    private WindCurrentMinMax windSpeed = new WindCurrentMinMax();
    private CurrentMinMax rain1H = new CurrentMinMax();
    private CurrentMinMax rain24H = new CurrentMinMax();

    private UnitValue rainAbsolute;
    private UnitValue rainIncrement;

    private Map<String, Object> stationValues = new HashMap<String, Object>();

    private int secondsToNextReading;
    private long totalTime = 0;

    /**
     * Copy constructor.
     * 
     * @param readings
     */
    public Readings(Readings readings) {

        super();
        copy(readings);
    }

    public Readings() {

        super();
    }

    public void copy(Readings readings) {

        copy(readings, false);
    }

    public void accumulate(Readings readings) {

        copy(readings, true);
    }

    private void copy(Readings readings, boolean accumulate) {

        readingTime = readings.readingTime;
        indoorTemperature.copy(readings.indoorTemperature);
        outdoorTemperature.copy(readings.outdoorTemperature);
        indoorHumidity.copy(readings.indoorHumidity);
        outdoorHumidity.copy(readings.outdoorHumidity);
        airPressure.copy(readings.airPressure);
        rain1H.copy(readings.rain1H);
        rain24H.copy(readings.rain24H);

        windSpeed.copy(readings.windSpeed);

        rainAbsolute = readings.rainAbsolute;
        if (accumulate) {
            accumulateRainIncrement(readings.rainIncrement);
        } else {
            rainIncrement = readings.rainIncrement;
            // if not accumulating readings then start from a fresh set of station values
            stationValues.clear();
        }

        stationValues.putAll(readings.stationValues);
    }

    public void average(Readings readings) {

        Date readingTime = readings.getReadingTime();
        long timeDiff = readingTime.getTime() - this.getReadingTime().getTime();

        // Average each reading.
        indoorTemperature.average(readings.indoorTemperature, totalTime, timeDiff, readingTime);
        outdoorTemperature.average(readings.outdoorTemperature, totalTime, timeDiff, readingTime);
        indoorHumidity.average(readings.indoorHumidity, totalTime, timeDiff, readingTime);
        outdoorHumidity.average(readings.outdoorHumidity, totalTime, timeDiff, readingTime);
        airPressure.average(readings.airPressure, totalTime, timeDiff, readingTime);
        windSpeed.average(readings.windSpeed, totalTime, timeDiff, readingTime);

        // Rain - absolute is just latest reading, increment is the sum of the increments.
        rainAbsolute = readings.rainAbsolute;
        accumulateRainIncrement(readings.rainIncrement);

        totalTime = totalTime + timeDiff;

        // Merge stationValues
        stationValues.putAll(readings.stationValues);
    }

    private void accumulateRainIncrement(UnitValue newIncrement) {

        rainIncrement = rainIncrement == null ? newIncrement : rainIncrement.add(newIncrement);
    }

    public boolean isDirty() {

        return (indoorTemperature.isDirty() || outdoorTemperature.isDirty() || indoorHumidity.isDirty()
                || outdoorHumidity.isDirty() || airPressure.isDirty() || windSpeed.isDirty() || rain1H.isDirty()
                || rain24H.isDirty() || (rainIncrement != null && rainIncrement.getValue().doubleValue() > 0.0));

    }

    public void clean() {

        indoorTemperature.clean();
        outdoorTemperature.clean();
        indoorHumidity.clean();
        outdoorHumidity.clean();
        airPressure.clean();
        windSpeed.clean();
        rain1H.clean();
        rain24H.clean();
        clearRainIncrement();
        totalTime = 0;

    }

    public String format(boolean printHeadings, boolean printCurrent, boolean printStationValues, boolean printMinMax) {

        StringBuilder sb = new StringBuilder();
        Formatter formatter = new Formatter(sb);

        if (printHeadings) {
            formatter.format(HEADING_FORMAT, "reading", "temp (in)", "temp (out)", "hum (in)", "hum (out)", "wind", "pressure",
                    "rain 1H", "rain 24H", "rain Abs", "rain Inc");
        }

        if (printCurrent) {
            formatter.format(VALUES_FORMAT, readingTime, indoorTemperature.getCurrent(), outdoorTemperature.getCurrent(),
                    indoorHumidity.getCurrent(), outdoorHumidity.getCurrent(), windSpeed.getCurrent(), airPressure.getCurrent(),
                    rain1H.getCurrent(), rain24H.getCurrent(), rainAbsolute, rainIncrement);

        }
        if (printStationValues) {
            for (Map.Entry<String, Object> entry : stationValues.entrySet()) {
                formatter.format("%s=%s\n", entry.getKey(), StringUtil.toString(entry.getValue()));
            }
        }

        if (printMinMax) {
            formatter.format(MIN_MAX_VALUES_FORMAT, "Minimums -", indoorTemperature.getMin().getUnitValue(), outdoorTemperature
                    .getMin().getUnitValue(), indoorHumidity.getMin().getUnitValue(), outdoorHumidity.getMin().getUnitValue(),
                    windSpeed.getMin().getUnitValue(), airPressure.getMin().getUnitValue(), rain1H.getMin().getUnitValue(),
                    rain24H.getMin().getUnitValue());

            Object[] dates = new Object[]{"recorded date", indoorTemperature.getMin().getTime(),
                    outdoorTemperature.getMin().getTime(), indoorHumidity.getMin().getTime(), outdoorHumidity.getMin().getTime(),
                    windSpeed.getMin().getTime(), airPressure.getMin().getTime(), rain1H.getMin().getTime(),
                    rain24H.getMin().getTime()};

            formatter.format(MIN_MAX_DATES_FORMAT, dates);
            dates[0] = "recorded time";
            formatter.format(MIN_MAX_TIMES_FORMAT, dates);

            formatter.format(MIN_MAX_VALUES_FORMAT, "Maximums -", indoorTemperature.getMax().getUnitValue(), outdoorTemperature
                    .getMax().getUnitValue(), indoorHumidity.getMax().getUnitValue(), outdoorHumidity.getMax().getUnitValue(),
                    windSpeed.getMax().getUnitValue(), airPressure.getMax().getUnitValue(), rain1H.getMax().getUnitValue(),
                    rain24H.getMax().getUnitValue());

            dates = new Object[]{"recorded date", indoorTemperature.getMax().getTime(), outdoorTemperature.getMax().getTime(),
                    indoorHumidity.getMax().getTime(), outdoorHumidity.getMax().getTime(), windSpeed.getMax().getTime(),
                    airPressure.getMax().getTime(), rain1H.getMax().getTime(), rain24H.getMax().getTime()};

            formatter.format(MIN_MAX_DATES_FORMAT, dates);
            dates[0] = "recorded time";
            formatter.format(MIN_MAX_TIMES_FORMAT, dates);
        }

        return sb.toString();
    }

    public String toString() {

        return format(true, true, true, true);
    }

    public CurrentMinMax getIndoorTemperature() {

        return indoorTemperature;
    }

    public CurrentMinMax getIndoorHumidity() {

        return indoorHumidity;
    }

    public CurrentMinMax getOutdoorTemperature() {

        return outdoorTemperature;
    }

    public CurrentMinMax getOutdoorHumidity() {

        return outdoorHumidity;
    }

    public CurrentMinMax getAirPressure() {

        return airPressure;
    }

    public CurrentMinMax getRain1H() {

        return rain1H;
    }

    public CurrentMinMax getRain24H() {

        return rain24H;
    }

    public WindCurrentMinMax getWindSpeed() {

        return windSpeed;
    }

    public Date getReadingTime() {

        return readingTime;
    }

    public void setReadingTime(Date readingTime) {

        this.readingTime = readingTime;
    }

    /**
     * Total rain since station last reset. Also calculates rainIncrement
     * 
     * @param newRainAbsolute
     *            value of rain
     * @param unit
     *            - RainfallUnit the value is measured in.
     */
    public void setRainAbsolute(double newRainAbsolute, RainfallUnit unit) {

        setRainAbsolute(new UnitValue(newRainAbsolute, unit));

    }

    /**
     * Total rain since station last reset. Also calculates rainIncrement
     * 
     * @param newRainAbsolute
     */
    public void setRainAbsolute(UnitValue newRainAbsolute) {

        Unit unit = newRainAbsolute.getUnit();
        double rainDiff = 0;
        if (rainAbsolute != null) {
            // We could expect the unit to be the same, but this means we can
            // change units if we want...
            rainDiff = (newRainAbsolute.getValue() - rainAbsolute.convertTo(unit).getValue());

            if (rainDiff < 0) {
                // Either initialising, or the station has reset itself.
                // We'll assume zero and start again.
                LOG.warn("Rain count reset. Assuming zero rain since last reading");
                rainDiff = 0;
            }
        }

        rainAbsolute = newRainAbsolute;
        rainIncrement = new UnitValue(rainDiff, unit);
    }

    public void clearRainIncrement() {

        rainIncrement = null;
    }

    /**
     * @return Rainfall between the last two calls to setRainAbsolute()
     */
    public UnitValue getRainIncrement() {

        return protectNull(rainIncrement);
    }

    public UnitValue getRainAbsolute() {

        return protectNull(rainAbsolute);
    }

    public void putStationValue(String key, Object value) {

        stationValues.put(key, value);

    }

    public Object removeStationValue(String key) {

        return stationValues.remove(key);

    }

    public Map<String, Object> getStationValues() {

        return Collections.unmodifiableMap(stationValues);
    }

    private UnitValue protectNull(UnitValue uv) {

        if (uv == null) {
            return UnitValue.NULL;
        }
        return uv;
    }

    public void setSecondsToNextReading(int secondsToNextReading) {

        this.secondsToNextReading = secondsToNextReading;

    }

    public int getSecondsToNextReading() {

        return secondsToNextReading;
    }

    @Override
    public int hashCode() {

        final int prime = 31;
        int result = 1;
        result = prime * result + ((airPressure == null) ? 0 : airPressure.hashCode());
        result = prime * result + ((indoorHumidity == null) ? 0 : indoorHumidity.hashCode());
        result = prime * result + ((indoorTemperature == null) ? 0 : indoorTemperature.hashCode());
        result = prime * result + ((outdoorHumidity == null) ? 0 : outdoorHumidity.hashCode());
        result = prime * result + ((outdoorTemperature == null) ? 0 : outdoorTemperature.hashCode());
        result = prime * result + ((rain1H == null) ? 0 : rain1H.hashCode());
        result = prime * result + ((rain24H == null) ? 0 : rain24H.hashCode());
        result = prime * result + ((rainAbsolute == null) ? 0 : rainAbsolute.hashCode());
        result = prime * result + ((rainIncrement == null) ? 0 : rainIncrement.hashCode());
        result = prime * result + ((readingTime == null) ? 0 : readingTime.hashCode());
        result = prime * result + secondsToNextReading;
        result = prime * result + ((stationValues == null) ? 0 : stationValues.hashCode());
        result = prime * result + ((windSpeed == null) ? 0 : windSpeed.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {

        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Readings other = (Readings) obj;
        if (airPressure == null) {
            if (other.airPressure != null)
                return false;
        } else if (!airPressure.equals(other.airPressure))
            return false;
        if (indoorHumidity == null) {
            if (other.indoorHumidity != null)
                return false;
        } else if (!indoorHumidity.equals(other.indoorHumidity))
            return false;
        if (indoorTemperature == null) {
            if (other.indoorTemperature != null)
                return false;
        } else if (!indoorTemperature.equals(other.indoorTemperature))
            return false;
        if (outdoorHumidity == null) {
            if (other.outdoorHumidity != null)
                return false;
        } else if (!outdoorHumidity.equals(other.outdoorHumidity))
            return false;
        if (outdoorTemperature == null) {
            if (other.outdoorTemperature != null)
                return false;
        } else if (!outdoorTemperature.equals(other.outdoorTemperature))
            return false;
        if (rain1H == null) {
            if (other.rain1H != null)
                return false;
        } else if (!rain1H.equals(other.rain1H))
            return false;
        if (rain24H == null) {
            if (other.rain24H != null)
                return false;
        } else if (!rain24H.equals(other.rain24H))
            return false;
        if (rainAbsolute == null) {
            if (other.rainAbsolute != null)
                return false;
        } else if (!rainAbsolute.equals(other.rainAbsolute))
            return false;
        if (rainIncrement == null) {
            if (other.rainIncrement != null)
                return false;
        } else if (!rainIncrement.equals(other.rainIncrement))
            return false;
        if (readingTime == null) {
            if (other.readingTime != null)
                return false;
        } else if (!readingTime.equals(other.readingTime))
            return false;
        if (secondsToNextReading != other.secondsToNextReading)
            return false;
        if (stationValues == null) {
            if (other.stationValues != null)
                return false;
        } else if (!stationValues.equals(other.stationValues))
            return false;
        if (windSpeed == null) {
            if (other.windSpeed != null)
                return false;
        } else if (!windSpeed.equals(other.windSpeed))
            return false;
        return true;
    }

}
